﻿
using EmperialApps.WeatherSpark.Data;
using EmperialApps.WeatherSpark.Internal;
using MultiTouch.ManipulationLib.Silverlight4;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Input.Manipulations;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

namespace EmperialApps.WeatherSpark {

    public sealed partial class WeatherGraph : WeatherControl {

        private const double TimeInterval = 3;
        private const double WindInterval = 5;
        private const double HorizontalOverhangAllowance = 50;
        private const double MaximumOffsetUpdate = 75;

        private readonly TimeLabelManager _timeLabels;
        private readonly TimeLabelManager _dateLabels;
        private readonly LabelManager _temperatureLabels;
        private readonly PressureLabelManager _pressureLabels;
        private readonly TimeGridLinesManager _timeGridLines;
        private readonly RowDefinition[] _dataRows;
        private readonly PanManager _pan;
        private readonly Storyboard _offsetStoryboard;
        private readonly Storyboard _referenceLineStoryboard;

        private Scale _timeScale;
        private Scale _temperatureScale;
        private Scale _pressureScale;
        private Scale _humidityScale;
        private TraceDataManager _temperatureData;
        private TraceDataManager _pressureData;
        private TraceDataManager _humidityData;
        private WindDataManager _windData;

        private Size _temperatureSize;
        private double _timeDisplayOffset;
        private double _maximumOffset;
        private double _descriptionOrigin;
        private double _descriptionFactor;
        private double _prevousOffset = -1;
        private bool _delayMoveReferenceLine;
        private bool _referenceLineTransitioning;

        public WeatherGraph( ) {
            if( DesignerProperties.IsInDesignTool )
                return;

            InitializeComponent( );

            var timeLabelTemplate = (DataTemplate)this.LayoutRoot.Resources["TimeLabelTemplate"];
            var dateLabelTemplate = (DataTemplate)this.LayoutRoot.Resources["DateLabelTemplate"];
            var temperatureLabelTemplate = (DataTemplate)this.LayoutRoot.Resources["TemperatureLabelTemplate"];
            var pressureLabelTemplate = (DataTemplate)this.LayoutRoot.Resources["PressureLabelTemplate"];
            this._timeLabels = new TimeLabelManager( timeLabelTemplate, this.TimeLabelContainer, HorizontalOverhangAllowance );
            this._dateLabels = new TimeLabelManager( dateLabelTemplate, this.GridLinesContainer, HorizontalOverhangAllowance ) { ShowHour = false };
            this._temperatureLabels = new LabelManager( temperatureLabelTemplate, this.TemperatureLabelContainer, false, 0 );
            this._pressureLabels = new PressureLabelManager( pressureLabelTemplate, this.PressureLabelContainer );

            Brush subtleBrush = (Brush)Application.Current.Resources["PhoneSubtleBrush"];
            double primaryLineThickness = (double)this.LayoutRoot.Resources["PrimaryLineThickness"];
            double tertiaryLineThickness = (double)this.LayoutRoot.Resources["TertiaryLineThickness"];
            this._timeGridLines = new TimeGridLinesManager( this.GridLinesContainer, subtleBrush, primaryLineThickness, tertiaryLineThickness );

            this._dataRows = new[] { this.TemperatureRow, this.PressureRow, this.HumidityRow, this.PrecipitationRow, this.CloudCoverRow, this.WindRow };

            this._pan = new PanManager( this );

            var offsetAnimation = this.Animate( "Offset", EasingMode.EaseOut );
            this._offsetStoryboard = new Storyboard { Children = { offsetAnimation } };
            this._offsetStoryboard.Completed += this.OnOffsetAnimationCompleted;

            var lineAnimation = this.ReferenceLine.EnsureTranslateTransform( ).Animate( "X", EasingMode.EaseInOut );
            var labelAnimation = this.TemperatureLabel.EnsureTranslateTransform( ).Animate( "X", EasingMode.EaseInOut );
            this._referenceLineStoryboard = new Storyboard { Children = { lineAnimation, labelAnimation } };
            this._referenceLineStoryboard.Completed += this.OnReferenceLineAnimationCompleted;

            this.TemperatureLabelContainer.SizeChanged += this.OnSizeChanged;
        }


        /// <summary>Moves the graph to the next or previous day.</summary>
        public void Move( bool forward ) {
            Forecast forecast = this.Forecast;
            if( forecast == null )
                return;

            var offsetAnimation = this.GetOffsetAnimation( );
            double offset = offsetAnimation.To ?? this.Offset;

            DateTimeOffset start = forecast.Start;
            DateTimeOffset current = start + TimeSpan.FromHours( this._timeScale.FromScreen( offset ) );
            DateTimeOffset moved = current + TimeSpan.FromHours( forward ? ConvertValue.HoursPerDay + TimeInterval : -TimeInterval );
            DateTimeOffset day = moved.GetDateOffset( );

            double timeDifference = day.HoursFrom( start );
            this.MoveOffset( this.Offset, timeDifference, 0 );
        }

        partial void CanMoveBackwardChanged( bool oldValue ) {
            this.OnMovementCapabilityChanged( EventArgs.Empty );
        }

        partial void CanMoveForwardChanged( bool oldValue ) {
            this.OnMovementCapabilityChanged( EventArgs.Empty );
        }


        private double GetDescriptionOffset( double offset ) {
            return (offset - this._descriptionOrigin) / this._descriptionFactor;
        }

        private DoubleAnimation GetOffsetAnimation( ) {
            return (DoubleAnimation)this._offsetStoryboard.Children[0];
        }

        private Pair<DoubleAnimation, DoubleAnimation> GetReferenceLineAnimations( ) {
            return Pair.Create(
                (DoubleAnimation)this._referenceLineStoryboard.Children[0],
                (DoubleAnimation)this._referenceLineStoryboard.Children[1] );
        }

        private bool MovingReferenceLine( ) {
            var animations = this.GetReferenceLineAnimations( );
            return animations.Item1.To.HasValue;
        }

        private void OnOffsetAnimationCompleted( object sender, EventArgs e ) {
            var offsetAnimation = this.GetOffsetAnimation( );
            offsetAnimation.To = null;
            this.OnMoveComplete( EventArgs.Empty );

            if( this._delayMoveReferenceLine ) {
                this._delayMoveReferenceLine = false;

                double offset = this.Offset;
                double descriptionOffset = this.GetDescriptionOffset( offset );
                this.MoveReferenceLine( offset, descriptionOffset );
            }
        }

        private void OnReferenceLineAnimationCompleted( object sender, EventArgs e ) {
            var animations = this.GetReferenceLineAnimations( );
            animations.Item1.To = animations.Item2.To = null;
            this._referenceLineTransitioning = false;

            // If we have switched to a fixed reference line, restore reference values for current time.
            if( !this.FollowingReferenceLine.GetValueOrDefault( ) ) {
                this.ReferenceLine.StrokeDashOffset = 0;

                var forecast = this.Forecast;
                var pressure = this.GetPressureValues( forecast );
                var windSpeed = this.GetWindSpeedValues( forecast );
                var temperature = this.GetTemperatureValues( forecast );
                double hourOffset = DateTimeOffset.Now.HoursFrom( forecast.Start );
                this.DisplayReferenceValues( forecast, temperature, windSpeed, pressure, hourOffset, showTime: false );
            }

            // Restore reference line state.
            OffsetCoerce( this, this.Offset );
            this._prevousOffset = -1;
            this.OnOffsetChanged( );
        }


        private void OnSizeChanged( object sender, SizeChangedEventArgs e ) {
            this.OnVisibleElementsChanged( );
        }

        protected override void OnVisibleElementsChanged( ) {
            ClipToSize( this.LayoutRoot );
            ClipToSize( this.TemperatureLabelContainer );

            // Save current size.
            this._temperatureSize = new Size(
                this.DataColumn.ActualWidth,
                this.TemperatureRow.ActualHeight );
            this._timeDisplayOffset = this.ActualWidth - this._temperatureSize.Width;

            // Set height of current time line.
            this.ReferenceLine.Y2 = this.GridLinesContainer.ActualHeight;

            // Block display of grid lines for non-data rows.
            var data = Pair.Create( this.ActualWidth, this.LayoutRoot.RowDefinitions.SkipWhile( row => row != this._dataRows[0] ) );
            this.GridLinesContainer.Update( Extensions.PathClip, data,
                ( t, clip, d ) => {
                    double width = d.Item1;
                    var rows = d.Item2;

                    double rowTop = 0;
                    foreach( var row in rows ) {
                        double rowBottom = row.ActualHeight + rowTop;
                        if( this._dataRows.Contains( row ) )
                            clip.Figures.Add( Display.CreateRectangleFigure( 0, width, rowTop, rowBottom ) );

                        rowTop = rowBottom;
                    }
                }
            );

            // Set offset for pressure grid lines.
            double pressureRowTop = data.Item2
                .TakeWhile( row => row != this.PressureRow )
                .Sum( row => row.ActualHeight );
            this.PressureGridLines.Translate( TranslateTransform.YProperty, pressureRowTop );

            // Show fixed horizontal grid lines around humidity row.
            this.HumidityGridLines.Update( Extensions.PathData, data,
                ( t, geometry, d ) => {
                    double width = d.Item1;
                    var rows = d.Item2;
                    double rowTop = rows
                        .TakeWhile( row => row != this.HumidityRow )
                        .Sum( row => row.ActualHeight );

                    double top = rowTop + 1;
                    double bottom = rowTop + this.HumidityRow.ActualHeight - 1;
                    double center = (bottom + top) / 2;

                    geometry.Figures.Add( CreateHorizontalLineFigure( 0, width, top ) );
                    geometry.Figures.Add( CreateHorizontalLineFigure( 0, width, bottom ) );
                    geometry.Figures.Add( CreateHorizontalLineFigure( 0, width, center ) );
                }
            );

            base.OnVisibleElementsChanged( );
        }

        partial void FollowingReferenceLineChanged( bool? oldValue ) {
            this._referenceLineTransitioning = oldValue.HasValue;
            bool followingReferenceLine = this.FollowingReferenceLine.GetValueOrDefault( );

            if( followingReferenceLine ) {
                this.ReferenceLine.StrokeDashOffset = 1;

                double offset = this.Offset;
                double descriptionOffset = this.GetDescriptionOffset( offset );
                this.MoveReferenceLine( offset, descriptionOffset );
            }
            else {
                this._prevousOffset = -1;
                this.OnOffsetChanged( );
            }
        }

        private void MoveReferenceLine( double offset, double descriptionOffset ) {
            bool followingReferenceLine = this.FollowingReferenceLine.GetValueOrDefault( );
            double currentOffset = this.ReferenceLine.EnsureTranslateTransform( ).X;
            double lineOffset = followingReferenceLine ? -this._descriptionOrigin : -offset;
            if( followingReferenceLine )
                descriptionOffset = 0;

            // If day move is in progress, only perform immediate update if mode was fixed.
            var offsetAnimation = this.GetOffsetAnimation( );
            if( this._referenceLineTransitioning && offsetAnimation.To.HasValue ) {
                if( followingReferenceLine )
                    this.SetReferenceLinePosition( -offset, descriptionOffset );

                this._delayMoveReferenceLine = true;
            }
            // If transitioning and new position is distant, animate changes.
            else if( this._referenceLineTransitioning && Math.Abs( currentOffset - lineOffset ) > 1 ) {
                var animations = this.GetReferenceLineAnimations( );
                if( animations.Item1.To != lineOffset ) {
                    // Disable day moves while animating.
                    this.CanMoveForward = this.CanMoveBackward = false;

                    animations.Item1.To = lineOffset;
                    if( !double.IsInfinity( descriptionOffset ) )
                        animations.Item2.To = descriptionOffset;

                    this._referenceLineStoryboard.Begin( );
                }
            }
            // Otherwise, update positions and values immediately.
            else {
                this.SetReferenceLinePosition( lineOffset, descriptionOffset );
                if( this._referenceLineTransitioning )
                    this.OnReferenceLineAnimationCompleted( this, EventArgs.Empty );
            }
        }

        private void SetReferenceLinePosition( double lineOffset, double descriptionOffset ) {
            this.ReferenceLine.Translate( TranslateTransform.XProperty, lineOffset );
            if( !double.IsInfinity( descriptionOffset ) )
                this.TemperatureLabel.Translate( TranslateTransform.XProperty, descriptionOffset );
        }


        private static object OffsetCoerce( DependencyObject d, object baseValue ) {
            var self = (WeatherGraph)d;
            double offset = (double)baseValue;

            const double minimumOffset = 0;

            bool canMoveBackward = offset > minimumOffset;
            bool canMoveForward = offset < self._maximumOffset;

            // Update day move state, if we are not moving the reference line.
            if( !self.MovingReferenceLine( ) ) {
                self.CanMoveBackward = canMoveBackward;
                self.CanMoveForward = canMoveForward;
            }

            double coerced;
            if( !canMoveBackward )
                coerced = minimumOffset;
            else if( !canMoveForward )
                coerced = self._maximumOffset;
            else
                coerced = offset;

            return coerced;
        }

        private void OnOffsetChanged( ) {
            double offset = this.Offset;
            Forecast forecast = this.Forecast;
            if( this._timeScale != null && forecast != null && this._prevousOffset != offset ) {
                this._prevousOffset = offset;

                this._timeLabels.DisplayVisuals( this._timeScale, TimeInterval, offset );
                this._dateLabels.DisplayVisuals( this._timeScale, ConvertValue.HoursPerDay, offset );
                this._timeGridLines.DisplayVisuals( this._timeScale, 2 * TimeInterval, offset );

                if( this._temperatureData == null ) {
                    var pressure = this.GetPressureValues( forecast );
                    var windSpeed = this.GetWindSpeedValues( forecast );
                    var temperature = this.GetTemperatureValues( forecast );
                    this._temperatureData = new TraceDataManager( temperature, this._temperatureScale, this._timeScale, this.ActualWidth );
                    this._pressureData = new TraceDataManager( pressure, this._pressureScale, this._timeScale, this.ActualWidth );
                    this._humidityData = new TraceDataManager( forecast.RelativeHumidity, this._humidityScale, this._timeScale, this.ActualWidth );
                    this._windData = new WindDataManager( windSpeed, forecast.WindDirection, this._timeScale, this.WindRow.ActualHeight, this.ActualWidth );

                    this.DisplayNightBackground( temperature );
                    ClipTrace( this.PrecipitationTrace, forecast.PrecipitationPotential, CreateProportionalCircle, this._timeScale, 1 );
                    ClipTrace( this.CloudCoverTrace, forecast.SkyCover, CreateProportionalSquare, this._timeScale, 1 );
                }

                this._temperatureData.DisplayTraceData( this.TemperatureTrace, offset, this._temperatureSize.Width );
                this._pressureData.DisplayTraceData( this.PressureTrace, offset, this._temperatureSize.Width );
                this._humidityData.DisplayTraceData( this.HumidityTrace, offset, this._temperatureSize.Width );
                this._windData.DisplayData( this.WindTrace, offset, this._temperatureSize.Width );

                double insetTranslateOffset = this._timeDisplayOffset - offset;
                this.NightBackground.Clip.Translate( TranslateTransform.XProperty, insetTranslateOffset );
                this.PrecipitationTrace.Clip.Translate( TranslateTransform.XProperty, insetTranslateOffset );
                this.CloudCoverTrace.Clip.Translate( TranslateTransform.XProperty, insetTranslateOffset );

                if( this.FollowingReferenceLine.GetValueOrDefault( ) && !this._delayMoveReferenceLine ) {
                    var temperature = this.GetTemperatureValues( forecast );
                    var windSpeed = this.GetWindSpeedValues( forecast );
                    var pressure = this.GetPressureValues( forecast );
                    this.DisplayReferenceValues( forecast, temperature, windSpeed, pressure, offset, showTime: true );
                }

                double descriptionOffset = this.GetDescriptionOffset( offset );
                if( !double.IsInfinity( descriptionOffset ) )
                    this.ForecastDescription.Translate( TranslateTransform.XProperty, descriptionOffset * 2 );

                this.MoveReferenceLine( offset, descriptionOffset );
            }
        }


        protected override Panel GetLayoutRoot( ) { return this.LayoutRoot; }

        protected override void DisplayForecastDescription( Place place, string nickname ) {
            this.ForecastDescription.Text = nickname + " ";
        }

        protected override void DisplayForecast( Forecast forecast, DateTimeOffset previousStart ) {
            if( forecast == null || this.TemperatureTrace == null || this.ActualHeight < 10 )
                return;

            this._prevousOffset = -1;
            this._temperatureData = null;

            // Set size of time scale so that one screen covers 24 hours.
            var pressure = this.GetPressureValues( forecast );
            var windSpeed = this.GetWindSpeedValues( forecast );
            var temperature = this.GetTemperatureValues( forecast );
            int dataCount = forecast.Interval * (temperature.Count - 1);
            double timeScreenSize = this._temperatureSize.Width * dataCount / ConvertValue.HoursPerDay;
            this._maximumOffset = timeScreenSize - this._temperatureSize.Width;

            // Initialize scales and visual managers.
            this._timeScale = new Scale( false, timeScreenSize, new double[] { 0, dataCount } );
            this._humidityScale = new Scale( true, this.HumidityRow.ActualHeight, new[] { 0.0, 1.0 } );
            this._pressureScale = AkimaInterpolation.GetInterpolatedScale( true, this.PressureRow.ActualHeight, pressure, this.Units == Units.Metric ? 0.5 : 0.0, 0 );

            this._temperatureScale = AkimaInterpolation.GetInterpolatedScale( true, this._temperatureSize.Height, temperature, 0.5 );
            if( Math.Abs( this._temperatureScale.Maximum % TemperatureInterval ) < 0.1 )
                this._temperatureScale = new Scale( true, this._temperatureSize.Height, new[] { this._temperatureScale.Minimum, this._temperatureScale.Maximum + 1 } );

            this._timeLabels.Start = forecast.Start;
            this._dateLabels.Start = forecast.Start;
            this._timeGridLines.Start = forecast.Start;

            // Display pressure visuals.
            if( this._pressureScale.ScreenSize > 0.0 ) {
                var pressureLabelValues = this._pressureLabels.DisplayVisuals( this._pressureScale, PressureInterval, 0 );
                this.DisplayGridLines( this.PressureGridLines, null, pressureLabelValues, this._timeScale, this._pressureScale, this._temperatureSize.Width );
            }

            // Display temperature visuals.
            var temperatureLabelValues = this._temperatureLabels.DisplayVisuals( this._temperatureScale, TemperatureInterval, 0 );
            double maximumPosition = this.DisplayTemperatureGridLines(
                this.MinorTemperatureGridLines, this.MajorTemperatureGridLines, this.ExtremeTemperatureLine,
                temperatureLabelValues, this._timeScale, this._temperatureScale, this._temperatureSize.Width );

            // Ensure date labels do not overlap temperature grid lines.
            double requiredSize = this.TimeLabelRow.ActualHeight;
            double actualSize = this._temperatureSize.Height - maximumPosition;
            double sizeOffset = this._temperatureSize.Height - requiredSize;
            if( actualSize < requiredSize )
                sizeOffset -= actualSize + 2;
            this._dateLabels.Margin = new Thickness( this._timeDisplayOffset, sizeOffset, 0, 0 );
            this._timeGridLines.ItemPositionOffset = this._timeDisplayOffset;

            // Remove size placeholder, if present.
            if( this.TimeLabelContainer.Children.Count == 1 )
                this.TimeLabelContainer.Children.Clear( );

            // Adjust offset to align with start of last forecast.
            double startOffset = this.Offset - this._timeScale.ToScreen( forecast.Start.HoursFrom( previousStart ) );
            double hourOffset = DateTimeOffset.Now.HoursFrom( forecast.Start );

            // Animate to current time, and set position of current time line.
            double currentTimeOffset;
            this.MoveOffset( startOffset, hourOffset ).GetValues( out currentTimeOffset, out this._descriptionOrigin );
            this.ReferenceLine.X1 = this.ReferenceLine.X2 = currentTimeOffset;

            double descriptionEnd = this._maximumOffset - this._descriptionOrigin;
            double descriptionSize = this.ActualWidth - this.ForecastDescription.ActualWidth;
            this._descriptionFactor = descriptionSize >= 0 ? -50 : 2 * descriptionEnd / descriptionSize;

            // Display current values.
            DisplayReferenceValues( forecast, temperature, windSpeed, pressure, hourOffset, showTime: false );
        }

        private void DisplayReferenceValues( Forecast forecast, DataValues temperature, DataValues windSpeed, DataValues pressure, double hourOffset, bool showTime ) {
            double referenceOffset;
            if( this.FollowingReferenceLine.GetValueOrDefault( ) ) {
                double referenceScreenOffset = this.Offset + this.ReferenceLine.X1 - this._descriptionOrigin;
                referenceOffset = this._timeScale.FromScreen( referenceScreenOffset );
            }
            else {
                referenceOffset = hourOffset;
            }

            int dataCount = forecast.Interval * (temperature.Count - 1);
            if( referenceOffset < dataCount ) {
                referenceOffset = Math.Max( 0, referenceOffset );
                string temperatureUnit = this.Units.GetTemperatureSymbol( );
                double currentTemperature = temperature.GetInterpolatedValue( referenceOffset, ConvertValue.Identity );
                this.CurrentTemperatureLabel.Text = string.Format( "{0:0.#}\u200a\u00b0{1}", currentTemperature, temperatureUnit );


                double low, high;
                DateTimeOffset hour = forecast.Start.AddHours( referenceOffset );
                var extremes = temperature.TryGetExtremeValues( forecast.Start, hour.GetDateOffset( ) );
                extremes.TryGetValues( out low, out high );
                this.CurrentTemperatureRangeLabel.Text =
                    extremes.HasValue
                        ? string.Format( " [\u200a{0:0.#}\u200a\u00b0{2}, {1:0.#}\u200a\u00b0{2}\u200a]", low, high, temperatureUnit )
                        : "";
                this.CurrentTimeLabel.Text =
                    showTime
                        ? hour.AddMinutes( Math.Round( hour.Minute / 10.0 ) * 10 - hour.Minute ).ToString( "t" )
                        : "";


                string pressureUnit = this.Units.GetPressureSymbol( );
                double currentPressure = pressure.GetInterpolatedValue( referenceOffset, ConvertValue.Identity );
                this.CurrentPressureLabel.Text = string.Format( " –  {0:0.0}\u200a{1}", currentPressure, pressureUnit );


                const string CurrentPercentLabelFormat = " –  {0:0.#}\u200a%";
                double currentHumidity = forecast.RelativeHumidity.GetInterpolatedValue( referenceOffset, ConvertValue.ToPercentage );
                this.CurrentHumidityLabel.Text = string.Format( CurrentPercentLabelFormat, currentHumidity );

                double currentPrecipitation = forecast.PrecipitationPotential.GetInterpolatedValue( referenceOffset, ConvertValue.ToPercentage );
                this.CurrentPrecipitationLabel.Text = string.Format( CurrentPercentLabelFormat, currentPrecipitation );

                double currentCloudCover = forecast.SkyCover.GetInterpolatedValue( referenceOffset, ConvertValue.ToPercentage );
                this.CurrentCloudCoverLabel.Text = string.Format( CurrentPercentLabelFormat, currentCloudCover );


                const string CurrentWindSpeedLabelFormat = " –  {0:0.#}\u200a{1}";
                const string CurrentWindLabelFormat = CurrentWindSpeedLabelFormat + ", {2}";
                string windSpeedUnits = this.Units == Units.Metric ? "m/s" : "mph";
                double currentWindSpeed = windSpeed.GetInterpolatedValue( referenceOffset, ConvertValue.Identity );
                if( Extensions.DistinguishZero( ref currentWindSpeed, 1 ) ) {
                    this.CurrentWindLabel.Text = string.Format( CurrentWindSpeedLabelFormat, 0, windSpeedUnits );
                }
                else {
                    double currentWindDirection = forecast.WindDirection.GetInterpolatedValue( referenceOffset, ConvertValue.Identity, ConvertValue.DegreesPerCircle );
                    CompassDirection currentCompassDirection = ConvertValue.ToCompassDirection( currentWindDirection );
                    this.CurrentWindLabel.Text = string.Format( CurrentWindLabelFormat, currentWindSpeed, windSpeedUnits, currentCompassDirection );
                }
            }
            else {
                this.CurrentTemperatureLabel.Text = "";
                this.CurrentTemperatureRangeLabel.Text = "";
                this.CurrentPressureLabel.Text = "";
                this.CurrentHumidityLabel.Text = "";
                this.CurrentPrecipitationLabel.Text = "";
                this.CurrentCloudCoverLabel.Text = "";
                this.CurrentWindLabel.Text = "";
            }
        }

        private Pair<double, double> MoveOffset( double startOffset, double hourOffset, double screenAdjustment = -1.0 / 3 ) {

            double hourScreenOffset = this._timeScale.ToScreen( hourOffset );
            double targetScreenOffset = hourScreenOffset + this._temperatureSize.Width * screenAdjustment;
            var offsetAnimation = this.GetOffsetAnimation( );
            if( Math.Round( offsetAnimation.To ?? -1 ) != Math.Round( targetScreenOffset ) ) {
                double duration = 0.25 + Math.Min( 375, Math.Abs( startOffset - hourScreenOffset ) ) / 250;
                offsetAnimation.From = startOffset;
                offsetAnimation.To = targetScreenOffset;
                offsetAnimation.Duration = new Duration( TimeSpan.FromSeconds( duration ) );

                this.Offset = startOffset + 0.001;  // Ensure initial data is drawn before starting animation.
                this._offsetStoryboard.Begin( );
            }
            else {
                offsetAnimation.To = targetScreenOffset;
            }

            return Pair.Create( hourScreenOffset, targetScreenOffset );
        }

        protected override void DisplayNightBackground( DataValues temperature ) {
            this.NightBackground.Update( Extensions.PathClip, temperature, this.ClipNightBackground );
        }

        private void ClipNightBackground( Rectangle background, PathGeometry clip, DataValues temperature ) {
            this.ClipNightBackground( clip, temperature, this._timeScale, this._temperatureScale );
        }


        private sealed class PressureLabelManager : LabelManager {
            private Pair<double, double> _range;

            public PressureLabelManager( DataTemplate labelTemplate, Grid labelPanel )
                : base( labelTemplate, labelPanel, isHorizontal: false, overhangAllowance: 20 ) { }

            public override IList<double> DisplayVisuals( Scale scale, double increment, double offset ) {
                this._range = Pair.Create( scale.Minimum, scale.Maximum );
                return base.DisplayVisuals( scale, increment, offset );
            }

            protected override double GetItemCenteringOffset( FrameworkElement item ) {
                int fraction;

                double value = GetItemValue( item );
                if( value == _range.Item1 )
                    fraction = 4;
                else if( value == _range.Item2 )
                    fraction = 1;
                else
                    fraction = 2;

                return item.ActualHeight * fraction / 5;
            }
        }

        private sealed class TimeGridLinesManager : VisualManager<Line> {
            private readonly Brush _lineBrush;
            private readonly double _dayLineThickness;
            private readonly double _hourLineThickness;

            public TimeGridLinesManager( Grid panel, Brush lineBrush, double dayLineThickness, double hourLineThickness )
                : base( CreateGridLine, panel, true, HorizontalOverhangAllowance ) {
                this._lineBrush = lineBrush;
                this._dayLineThickness = dayLineThickness;
                this._hourLineThickness = hourLineThickness;
            }

            public DateTimeOffset Start { get; set; }
            public double ItemPositionOffset { get; set; }

            protected override double GetRangeStart( double minimum, double increment ) {
                var time = Start.AddHours( minimum );
                double start = base.GetRangeStart( time.Hour, increment ) - time.Hour;
                return start;
            }

            private static object CreateGridLine( ) {
                return new Line {
                    HorizontalAlignment = HorizontalAlignment.Left,
                    Stretch = Stretch.Fill,
                    Y2 = 1
                };
            }

            protected override void SetItemValue( Line item, double value ) {
                item.Tag = value;
                var time = Start.AddHours( value );

                item.Stroke = this._lineBrush;
                item.StrokeThickness =
                    time.Hour == 0
                        ? this._dayLineThickness
                        : this._hourLineThickness;
            }

            protected override void SetItemPosition( Line item, double itemPosition ) {
                base.SetItemPosition( item, itemPosition + ItemPositionOffset );
            }

            protected override double GetItemSize( Line item ) {
                return item.StrokeThickness;
            }
        }

        private abstract class DataManager<TData, TCollection>
            where TCollection : PresentationFrameworkCollection<TData>, new( ) {
            private readonly TCollection[] _collections;
            private int _index;

            protected DataManager( IEnumerable<TData> data, double displayLimit, double maximumPosition ) {
                this._collections = this.SplitPoints( data, displayLimit, maximumPosition );
            }

            private TCollection CurrentTracePoints {
                get { return this._collections[this._index]; }
            }

            protected abstract double GetHorizontalPosition( TData value );

            protected virtual TData Clone( TData value ) { return value; }

            protected TCollection GetCurrentTracePoints( double offset, double displayLimit ) {
                while( this._index > 0 && offset < this.GetHorizontalPosition( this.CurrentTracePoints[0] ) )
                    --this._index;

                double end = offset + displayLimit + 1;
                while( this._index < this._collections.Length - 1
                    && end >= this.GetHorizontalPosition( this.CurrentTracePoints.Last( ) ) )
                    ++this._index;

                return this.CurrentTracePoints;
            }

            private TCollection[] SplitPoints( IEnumerable<TData> data, double displayLimit, double maximumPosition ) {
                double factor = Math.Pow( 10, Math.Floor( Math.Log10( displayLimit ) ) );
                double overlap = Math.Ceiling( displayLimit / factor ) * factor;

                int i = 0;
                double end = maximumPosition;
                var current = new TCollection( );
                var splitPoints = new List<TCollection> { current };
                foreach( TData value in data ) {
                    ++i;
                    double x = this.GetHorizontalPosition( value );
                    if( x > end ) {
                        // If we've reached the end of the current collection, create a new one.
                        var previous = current;
                        current = new TCollection( );
                        splitPoints.Add( current );

                        // Pre-populate with sufficient overlap so that screen is always covered.
                        double begin = end - overlap;
                        end = begin + maximumPosition;
                        foreach( TData overlapValue in previous ) {
                            double overlapX = this.GetHorizontalPosition( overlapValue );
                            if( overlapX >= begin )
                                current.Add( Clone( overlapValue ) );
                        }
                    }

                    current.Add( value );
                }

                //var cs = splitPoints.Select( c => c.Count ).ToArray( );
                return splitPoints.ToArray( );
            }
        }

        private sealed class TraceDataManager : DataManager<Point, PointCollection> {
            private const double MaximumPointPosition = 1800;

            public TraceDataManager( DataValues values, Scale dataScale, Scale timeScale, double displayLimit )
                : this( GetInterpolatedPoints( values, dataScale, timeScale ), displayLimit ) { }

            public TraceDataManager( IEnumerable<Point> points, double displayLimit )
                : base( points, displayLimit, MaximumPointPosition ) { }

            public void DisplayTraceData( Polyline trace, double offset, double displayLimit ) {
                trace.Points = this.GetCurrentTracePoints( offset, displayLimit );
                trace.Translate( TranslateTransform.XProperty, -offset );
            }

            protected override double GetHorizontalPosition( Point value ) {
                return value.X;
            }

            private static IEnumerable<Point> GetInterpolatedPoints( DataValues values, Scale dataScale, Scale timeScale ) {
                var interpolation = new AkimaInterpolation( values.Select( dataScale.ToScreen ), values.Interval );
                var points = interpolation.Interpolate( timeScale ).Select( p => new Point( p.Item1, p.Item2 ) );
                return points;
            }
        }

        private sealed class WindDataManager : DataManager<PathFigure, PathFigureCollection> {
            private const double MaximumFigurePosition = 900;

            private static readonly double BarbAngle = ConvertValue.ToRadians( 30 );
            private static readonly Point Barb = new Point(
                Math.Cos( BarbAngle ),
                Math.Sin( BarbAngle ) );

            public WindDataManager( DataValues speedValues, DataValues directionValues, Scale timeScale, double rowHeight, double displayLimit )
                : base( GetWindBarbs( speedValues, directionValues, timeScale, rowHeight ), displayLimit, MaximumFigurePosition ) { }

            public void DisplayData( Path trace, double offset, double displayLimit ) {
                var data = trace.Ensure( Extensions.PathData );
                data.Figures = this.GetCurrentTracePoints( offset, displayLimit );
                trace.Translate( TranslateTransform.XProperty, -offset );
            }

            protected override double GetHorizontalPosition( PathFigure value ) {
                return value.StartPoint.X;
            }

            protected override PathFigure Clone( PathFigure value ) {
                var segment = (PolyLineSegment)value.Segments[0];
                var points = Clone( segment.Points );
                Point start = value.StartPoint;

                return CreateFigure( start, points );
            }

            private static PointCollection Clone( PointCollection points ) {
                var clone = new PointCollection( );
                foreach( Point point in points )
                    clone.Add( point );

                return clone;
            }

            private static PathFigure CreateFigure( Point start, PointCollection points ) {
                var segment = new PolyLineSegment { Points = points };
                var figure = new PathFigure { StartPoint = start, IsClosed = true, Segments = { segment } };
                return figure;
            }

            private static IEnumerable<PathFigure> GetWindBarbs( DataValues speedValues, DataValues directionValues, Scale timeScale, double rowHeight ) {
                int count = speedValues.Count;
                double y = rowHeight / 2;
                for( int i = 0; i < count; ++i ) {
                    double speed = speedValues[i];
                    double direction = directionValues[i];
                    double x = timeScale.ToScreen( i * speedValues.Interval );
                    var start = new Point( x, y );

                    var points = new PointCollection( );
                    DrawWind( points, speed, x, y );
                    RotatePoints( points, direction, start );

                    yield return CreateFigure( start, points );
                }
            }

            private static void DrawWind( PointCollection points, double speed, double x, double y ) {
                // Round speed to nearest whole value.
                if( speed > 0 )
                    speed = Math.Max( 1, Math.Round( speed ) );

                double barbLength = 0.2 * y;
                double flagHeight = 2 * barbLength * Barb.Y;
                double radius = y - flagHeight - 2;
                double step = radius / 6;

                // Draw flags for every fifty units of speed, and barbs for the remaining values less than fifty (if there is room).
                double yOffset = y - radius;
                if( DrawWindFlags( points, speed / 50, x, ref yOffset, barbLength, step, flagHeight ) )
                    DrawWindBarbs( points, speed % 50, x, ref yOffset, barbLength, step );
            }

            private static bool DrawWindFlags( PointCollection points, double flagSpeed, double x, ref double yOffset, double barbLength, double step, double flagHeight ) {
                bool drawBarbs = flagSpeed > 0;

                // Draw flags for every fifty units up one side and down the other.
                int flagCount = (int)flagSpeed;
                int flagStart = Math.Max( -3, 1 - flagCount );
                int flagEnd = Math.Min( 5, flagStart + flagCount );
                if( flagEnd == 3 ) {    // draw 300 symmetrically
                    ++flagStart;
                    ++flagEnd;
                }

                double flagWidth = 2 * barbLength * Barb.X;
                double flagYOffset = yOffset - flagStart * flagHeight;
                for( int flag = flagStart; flag < flagEnd; ++flag ) {
                    // If flag is on the right hand side, add flag tip.
                    if( flag > 0 ) {
                        points.Add( new Point( x + flagWidth, flagYOffset ) );
                        flagYOffset += flagHeight;

                        // If we have one flag on the right side, draw barbs one notch lower.
                        if( flag == 1 )
                            yOffset += step;
                        // If we have multiple flags on the right side, don't draw barbs.
                        else
                            drawBarbs = false;
                    }

                    // Add flag base.
                    points.Add( new Point( x, flagYOffset ) );

                    // If flag is on left hand side, add flag tip.
                    if( flag <= 0 ) {
                        flagYOffset -= flagHeight;
                        points.Add( new Point( x - flagWidth, flagYOffset ) );

                        // If this is the last flag on the left hand side, cap flag and move to barb position.
                        if( flag == 0 && flagEnd == 1 ) {
                            points.Add( new Point( x, flagYOffset ) );
                            points.Add( new Point( x, yOffset ) );
                        }
                    }
                }

                return drawBarbs;
            }

            private static void DrawWindBarbs( PointCollection points, double barbSpeed, double x, ref double yOffset, double barbLength, double step ) {
                // Draw barbs for:
                //  10s: multiple 2 - large barb, 10 ≤ speed < 50
                //   5s: multiple 1 - small barb,  5 ≤ speed < 10
                //   1s: multiple 0 -    no barb,  0 < speed < 5 (just direction)
                for( int multiple = 2; barbSpeed > 0 && multiple >= 0; --multiple ) {
                    double interval = multiple * WindInterval;
                    while( barbSpeed >= interval ) {
                        points.Add( new Point( x, yOffset ) );
                        if( multiple > 0 ) {
                            points.Add( new Point(
                                x + multiple * barbLength * Barb.X,
                                yOffset - multiple * barbLength * Barb.Y ) );
                            points.Add( new Point( x, yOffset ) );

                            barbSpeed -= interval;
                        }
                        else {
                            break;
                        }

                        yOffset += step;
                    }
                }
            }

            private static void RotatePoints( PointCollection points, double direction, Point origin ) {
                double angle = ConvertValue.ToRadians( direction );
                Matrix matrix = Extensions.CreateRotationMatrix( angle, origin );

                if( !matrix.IsIdentity ) {
                    for( int j = 0; j < points.Count; ++j )
                        points[j] = matrix.Transform( points[j] );
                }
            }
        }

        // (based on MultiTouchManipulationBehavior.cs implementation: http://multitouch.codeplex.com/SourceControl/changeset/view/86637#975939)
        private sealed class PanManager {
            private const float DefaultDpi = 96.0f;
            private const float Deceleration = 7.5f * DefaultDpi / (1000.0f * 1000.0f);
            private const float MinimumFlickVelocity = 2.0f * DefaultDpi / 1000.0f;
            private const float MaximumFlickVelocityFactor = 15f;

            private readonly WeatherGraph _associatedObject;
            private readonly ManipulationProcessor2D _manipulationProcessor;
            private readonly InertiaProcessor2D _inertiaProcessor;
            private bool _isMouseCaptured;

            public PanManager( WeatherGraph associatedObject ) {
                this._associatedObject = associatedObject;
                this._associatedObject.Loaded += this.OnAssociatedObjectLoaded;
                this._associatedObject.Unloaded += this.OnAssociatedObjectUnloaded;
                this._associatedObject.MouseLeftButtonDown += this.OnMouseDown;
                this._associatedObject.MouseLeftButtonUp += this.OnMouseUp;
                this._associatedObject.MouseMove += this.OnMouseMove;
                this._associatedObject.LostMouseCapture += this.OnLostMouseCapture;

                this._manipulationProcessor = new ManipulationProcessor2D( Manipulations2D.TranslateX );
                this._manipulationProcessor.Started += this.OnManipulationStarted;
                this._manipulationProcessor.Delta += this.OnManipulationDelta;
                this._manipulationProcessor.Completed += this.OnManipulationCompleted;

                this._inertiaProcessor = new InertiaProcessor2D { TranslationBehavior = { DesiredDeceleration = Deceleration } };
                this._inertiaProcessor.Delta += this.OnManipulationDelta;
                this._inertiaProcessor.Completed += this.OnInertiaCompleted;
            }

            /// <summary>Gets the current timestamp.</summary>
            private static long Timestamp {
                get {
                    // The question of what tick source to use is a difficult
                    // one in general, but for purposes of this test app,
                    // DateTime ticks are good enough.
                    return DateTime.UtcNow.Ticks;
                }
            }

            private void OnAssociatedObjectLoaded( object sender, RoutedEventArgs e ) {
                TouchHelper.SetRootElement( TouchHelper.GetRootElement( this._associatedObject ) );
                TouchHelper.AddHandlers( this._associatedObject, new TouchHandlers {
                    TouchDown = this.OnTouchDown,
                    CapturedTouchReported = this.OnCapturedTouchReported,
                    CapturedTouchUp = this.OnTouchUp
                } );
                TouchHelper.EnableInput( true );
            }

            private void OnAssociatedObjectUnloaded( object sender, RoutedEventArgs e ) {
                TouchHelper.EnableInput( false );
                TouchHelper.RemoveHandlers( this._associatedObject );
            }

            #region Mouse handlers

            private void OnMouseDown( object sender, MouseButtonEventArgs e ) {
                // ignore mouse if there are any touches
                if( !TouchHelper.AreAnyTouches && this._associatedObject.CaptureMouse( ) ) {
                    this._isMouseCaptured = true;
                    this.ProcessMouse( e );
                    e.Handled = true;
                }
            }

            private void OnMouseUp( object sender, MouseButtonEventArgs e ) {
                if( this._isMouseCaptured ) {
                    this._associatedObject.ReleaseMouseCapture( );
                    e.Handled = true;
                }
            }

            private void OnMouseMove( object sender, MouseEventArgs e ) {
                if( this._isMouseCaptured ) {
                    // ignore mouse if there are any touches
                    if( TouchHelper.AreAnyTouches )
                        this._associatedObject.ReleaseMouseCapture( );
                    else
                        this.ProcessMouse( e );
                }
            }

            private void OnLostMouseCapture( object sender, MouseEventArgs e ) {
                if( this._isMouseCaptured ) {
                    this._manipulationProcessor.ProcessManipulators( Timestamp, null );
                    this._isMouseCaptured = false;
                }
            }

            /// <summary>Process a mouse event. Note: mouse and touches at the same time are not supported.</summary>
            private void ProcessMouse( MouseEventArgs e ) {
                var parent = this._associatedObject.Parent as UIElement;
                if( parent == null )
                    return;

                var position = e.GetPosition( parent );
                var manipulators = new[] { new Manipulator2D( 0, (float)position.X, (float)position.Y ) };

                this._manipulationProcessor.ProcessManipulators( Timestamp, manipulators );
            }

            #endregion

            #region Touch handlers

            /// <summary>Occurs when a Touch Point is pressed.</summary>
            private void OnTouchDown( object sender, TouchEventArgs e ) {
                System.Diagnostics.Debug.WriteLine( "TouchPoint {0} Down at ({1})", e.TouchPoint.TouchDevice.Id, e.TouchPoint.Position );
                e.TouchPoint.TouchDevice.Capture( this._associatedObject );
            }

            /// <summary>Occurs when a Touch Point is released.</summary>
            private void OnTouchUp( object sender, TouchEventArgs e ) {
                System.Diagnostics.Debug.WriteLine( "TouchPoint {0} Up at ({1})", e.TouchPoint.TouchDevice.Id, e.TouchPoint.Position );

                // Workaround for the Touch.FrameReported issue in WP7 not releasing Touch Points
                TouchHelper.ResetTouchPoints( );
            }

            /// <summary>Occurs when Touch points are reported: handles manipulations.</summary>
            private void OnCapturedTouchReported( object sender, TouchReportedEventArgs e ) {
                var parent = this._associatedObject.Parent as UIElement;
                if( parent == null )
                    return;

                // Find the root element
                var root = TouchHelper.GetRootElement( parent );
                if( root == null )
                    return;

                // Multi-Page support: verify if the collection of Touch points is null
                var touchPoints = e.TouchPoints;
                List<Manipulator2D> manipulators = null;

                if( touchPoints.Any( ) ) {
                    // get transformation to convert positions to the parent's coordinate system
                    var transform = root.TransformToVisual( parent );

                    foreach( var touchPoint in touchPoints ) {
                        // convert to the parent's coordinate system
                        var position = transform.Transform( touchPoint.Position );

                        // create a manipulator
                        if( manipulators == null )
                            manipulators = new List<Manipulator2D>( );
                        var manipulator = new Manipulator2D( touchPoint.TouchDevice.Id, (float)position.X, (float)position.Y );
                        manipulators.Add( manipulator );

                        //System.Diagnostics.Debug.WriteLine( "TouchPoint {0} Reported at ({1}); Total Touch Points: {2}", touchPoint.TouchDevice.Id, touchPoint.Position, touchPoints.Count( ) );
                    }
                }

                // process manipulations
                this._manipulationProcessor.ProcessManipulators( Timestamp, manipulators );
            }

            #endregion

            #region Manipulation handlers

            /// <summary>Stops inertia.</summary>
            private void StopInertia( ) {
                if( this._inertiaProcessor.IsRunning )
                    this._inertiaProcessor.Complete( Timestamp );
            }

            private void WaitForRender( ) {
                if( !this._waitingForRender ) {
                    this._waitingForRender = true;
                    CompositionTarget.Rendering += this.OnCompositionTargetRendering;
                }
            }

            /// <summary>Here when manipulation starts.</summary>
            private void OnManipulationStarted( object sender, Manipulation2DStartedEventArgs e ) {
                this.StopInertia( );
                this._offset = this._associatedObject.Offset;
            }

            /// <summary>Here when manipulation gives a delta.</summary>
            private void OnManipulationDelta( object sender, Manipulation2DDeltaEventArgs e ) {
                this._offset -= e.Delta.TranslationX;
                this.WaitForRender( );
            }

            private double _offset;
            private bool _waitingForRender;
            private void OnCompositionTargetRendering( object sender, EventArgs e ) {
                if( this._inertiaProcessor.IsRunning ) {
                    this._inertiaProcessor.Process( Timestamp );
                }
                else {
                    this._waitingForRender = false;
                    CompositionTarget.Rendering -= this.OnCompositionTargetRendering;
                }

                double update = this._offset - this._associatedObject.Offset;
                if( update > MaximumOffsetUpdate )
                    update = MaximumOffsetUpdate;
                else if( update < -MaximumOffsetUpdate )
                    update = -MaximumOffsetUpdate;

                this._associatedObject.Offset += update;
            }

            /// <summary>Here when manipulation completes.</summary>
            private void OnManipulationCompleted( object sender, Manipulation2DCompletedEventArgs e ) {
                // Get the initial inertia values
                var initialVelocity = new Vector( e.Velocities.LinearVelocityX, 0 );

                // set initial velocity if translate flicks are allowed
                double velocityLengthSquared = initialVelocity.LengthSquared;
                if( velocityLengthSquared > MinimumFlickVelocity * MinimumFlickVelocity ) {
                    const double MaximumLengthSquared = MaximumFlickVelocityFactor * MinimumFlickVelocity * MinimumFlickVelocity;
                    if( velocityLengthSquared > MaximumLengthSquared )
                        initialVelocity = Math.Sqrt( MaximumLengthSquared / velocityLengthSquared ) * initialVelocity;

                    this._inertiaProcessor.TranslationBehavior.InitialVelocityX = (float)initialVelocity.X;
                    this._inertiaProcessor.Process( Timestamp );
                    this.WaitForRender( );
                }
            }

            /// <summary>Here when manipulation completes.</summary>
            private void OnInertiaCompleted( object sender, Manipulation2DCompletedEventArgs e ) {
                System.Diagnostics.Debug.WriteLine( "Inertia completed." );
                this._inertiaProcessor.TranslationBehavior.InitialVelocityX = 0f;
            }

            #endregion
        }

    }

}
